pdf檔也是常見的檔案格式,這次的範例是想嘗試在Spring boot的環境中,查詢實際的DB,將資料整理後匯出pdf。
想模擬的情境是學校學生成績的相關報表。
 
 
以SQL查詢Student Table,確認是我們要的資料
SELECT * FROM ithome2024.student;
在MySQl中看到原始資料大概有這些
我常使用JPA搭配Querydsl,先建立映射對象Entity
@Entity
@Table(name = "student", schema = "ithome2024")
public class StudentEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "Student_Id")
    private Integer studentId;
    @Column(name = "First_Name")
    private String firstName;
    @Column(name = "Last_Name")
    private String lastName;
    @Column(name = "Gender")
    private String gender;
    @Column(name = "Grade")
    private String grade;
    @Column(name = "Department_Id")
    private Integer departmentId;
}
Querydsl很像SQL語法,還可以透過Projections.bean自動將查詢結果的列對應到DTO的屬性上。
@Repository
public class JasperReportDemoDaoImpl implements IJasperReportDemoDao {
    @Autowired
    private JPQLQueryFactory queryFactory;
    @Override
    public List<StudentAndDepartmentDto> getStudentAndDepartmentData() {
        QStudentEntity qStudent = QStudentEntity.studentEntity;
        QDepartmentEntity qDepartment = QDepartmentEntity.departmentEntity;
        return queryFactory.select(Projections
                    .bean(StudentAndDepartmentDto.class,
                    qStudent.studentId, qStudent.firstName, qStudent.lastName,
                    qStudent.gender, qStudent.grade,
                    qDepartment.departmentId, qDepartment.departmentName,
                    qDepartment.departmentDesc))
                .from(qStudent)
                .innerJoin(qDepartment)
                .on(qStudent.departmentId.eq(qDepartment.departmentId))
                .fetch();
    }
}
由於這次情境很單純,Service沒有什麼邏輯要處理,僅是做一些資料轉換
@Service
public class ReportDemoServiceImpl implements IReportDemoService {
    @Autowired
    private IJasperReportDemoDao jasperReportDemoDao;
    @Override
    public List<StudentDataReportModel> getStudentAndDepartmentData() {
        List<StudentDataReportModel> studentDataReportModelList = null;
        try {
            studentDataReportModelList = Optional
                    .of(jasperReportDemoDao.getStudentAndDepartmentData())
                    .orElse(new ArrayList<>())
                    .stream().map(StudentDataReportModel::new)
                    .collect(Collectors.toList());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return studentDataReportModelList;
    }
}
資料轉換的部分放在Model的建構子中
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentDataReportModel {
    private Integer studentId;
    private String fullName;
    private String gender;
    private String grade;
    private String departmentDesc;
    public StudentDataReportModel(StudentAndDepartmentDto dto) {
        this.studentId = dto.getStudentId();
        this.fullName = dto.getFirstName() + " " + dto.getLastName();
        this.gender = "Male".equals(dto.getGender()) ? "男" : "女";
        this.grade = dto.getGrade();
        this.departmentDesc = dto.getDepartmentDesc();
    }
}
List<StudentDataReportModel>就是這次要放進報表的DataSource// 1. 查詢學生基本資料
List<StudentDataReportModel> studentDataReportModelList = 
                      reportDemoService.getStudentAndDepartmentData();
// 2. 設定報表參數
Map<String, Object> parametersMap = 
                this.getStudentDataParameters(studentDataReportModelList);
                    
private Map<String, Object> getStudentDataParameters(
            List<StudentDataReportModel> studentDataReportModelList) {
    Map<String, Object> parameters = new HashMap<>();
    
    LocalDate localDate = new Date().toInstant()
                        .atZone(ZoneId.systemDefault()).toLocalDate();
    parameters.put("date", localDate
                   .format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
                   
    parameters.put("studentNum", studentDataReportModelList.size());
    return parameters;
}
public class ExportReportUtil {
    // 以DataSource、報表路徑、parametersMap作為參數,在Util中處理報表生命週期
    public static byte[] templateToPdfByteSimple(List dataSourceList, 
                String reportPath, Map<String, Object> parametersMap
                                                    ) throws Exception {
        try {
            // 以JasperCompileManager將jrxml模板編譯成jasper文件
            JasperReport jasperReport = JasperCompileManager
                                .compileReport(ExportReportUtil
                                .class.getResourceAsStream(reportPath));
            // 將Java集合資料來源與Jasper報表進行綁定
            JRDataSource dataSource = 
                        new JRBeanCollectionDataSource(dataSourceList, true);
            // 將資料填入報表
            JasperPrint print = JasperFillManager
                       .fillReport(jasperReport, parametersMap, dataSource);
            // 匯出為pdf
            return JasperExportManager.exportReportToPdf(print);
        } catch (Exception e) {
            throw new Exception();
        }
    }
}
方法中宣告模板路徑,並將第一步的DataSource、第二步的parametersMap都作為參數傳入templateToPdfByteSimple方法
// 3.匯出excel byte[]
byte[] bytes = null;
try {
    String reportPath = "/Report/Jasper/StudentDataReport.jrxml";
    bytes = ExportReportUtil
                .templateToPdfByteSimple(studentDataReportModelList, 
                                          reportPath, parametersMap);
} catch (Exception e) {
    throw new RuntimeException(e);
}
// 4.設定檔案名稱
String encodedFilename = null;
try {
    encodedFilename = URLEncoder
        .encode("學生與科系資料." + fileType, StandardCharsets.UTF_8.name());
} catch (Exception e) {
    throw new RuntimeException(e);
}
最後打開匯出的pdf,但越看越不對勁...
中文字都不見了!
google之後發現這個問題存在已久,也有不少解決方案,就在下一篇說明吧